ExpressBeans   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
eloc 93
dl 0
loc 162
ccs 54
cts 54
cp 1
c 0
b 0
f 0
rs 10

8 Functions

Rating   Name   Duplication   Size   Complexity  
A registerRouters 0 24 2
A createApp 0 8 1
A listen 0 14 3
C serializeRequest 0 16 10
A checkRouterBeans 0 17 2
A getApp 0 7 1
A use 0 7 1
A initialize 0 27 3
1 8
import express, { Express, Request, Router } from 'express';
2 8
import { pinoHttp, startTime } from 'pino-http';
3
import { ServerResponse, IncomingMessage } from 'http';
4 8
import EventEmitter from 'events';
5
import { ExpressBeansOptions, ExpressRouterBean } from '@/ExpressBeansTypes';
6 8
import { logger } from '@/core';
7 8
import { Executor } from '@/core/executor';
8 8
import { Objects } from '@/utils/Objects';
9
10
type ExpressBeanEventMap = {
11
  error: [Error];
12
  initialized: [];
13
}
14
15 8
export default class ExpressBeans extends EventEmitter<ExpressBeanEventMap> {
16 22
  private readonly app: Express;
17
18 22
  private readonly router: Router;
19
20
  /**
21
   * Creates a new ExpressBeans application
22
   * @param options {ExpressBeansOptions}
23
   */
24
  static async createApp(options?: Partial<ExpressBeansOptions>): Promise<ExpressBeans> {
25 6
    const app = new ExpressBeans({ ...options });
26 6
    return app;
27
  }
28
29
  /**
30
   * Constructor of ExpressBeans application
31
   * @constructor
32
   * @param options {ExpressBeansOptions}
33
   */
34
  constructor(options?: Partial<ExpressBeansOptions>) {
35 22
    super();
36 22
    this.router = express.Router();
37 22
    this.app = express();
38 22
    this.app.disable('x-powered-by');
39 22
    this.app.use(options?.baseURL ?? '/', this.router);
40 22
    if (options?.logRequests === undefined || options.logRequests) {
41 20
      this.router.use(pinoHttp(
42
        {
43
          logger,
44
          customSuccessMessage: this.serializeRequest.bind(this),
45
          customErrorMessage: this.serializeRequest.bind(this),
46
        },
47
      ));
48
    }
49 22
    Executor.setExecution('run', () => this.initialize(options ?? {}));
50 22
    Executor.on('error', () => {
51 6
      process.exit(1);
52
    });
53 22
    Executor.startLifecycle();
54
  }
55
56
  private serializeRequest(req: IncomingMessage, res: ServerResponse) {
57 12
    const request: Request = req as Request;
58 12
    const remoteAddress = request.headers['x-forwarded-for'] ?? request.socket.remoteAddress;
59 12
    const { method, originalUrl, httpVersion } = request;
60 12
    const responseTime = Date.now() - res[startTime];
61 12
    const optionals = [
62
      res.statusCode,
63
      res.getHeader('content-length'),
64
      res.getHeader('content-type'),
65
      request.headers.referer,
66
      request.headers['user-agent'],
67
    ]
68
      .filter(Objects.nonNullish)
69
      .join(' ');
70 12
    return `${remoteAddress} - "${method} ${originalUrl} HTTP/${httpVersion}" ${optionals} - ${responseTime}ms`;
71
  }
72
73
  /**
74
   * Initializes the application and checks
75
   * if all beans are valid
76
   * @param listen {boolean}
77
   * @param port {number}
78
   * @param beans {Object[]}
79
   * @param onInitialized {Function}
80
   * @private
81
   */
82 19
  private async initialize({
83
    listen = true,
84
    port = 8080,
85
    routerBeans = [],
86
  }: Partial<ExpressBeansOptions>) {
87 19
    return Promise.resolve(routerBeans as Array<ExpressRouterBean>)
88
      .then(this.checkRouterBeans.bind(this))
89
      .then(this.registerRouters.bind(this))
90
      .then(() => {
91 17
        if (listen) {
92 6
          return new Promise<void>(
93
            (resolve, reject) => {
94 6
              this.listen(port, (err) => (err ? reject(err) : resolve()));
95
            },
96 4
          ).catch((err: Error) => Promise.reject(err));
97
        }
98 11
        return Promise.resolve();
99
      });
100
  }
101
102
  /**
103
   * Starts the server and emits the initialized event
104
   * @param {number} port
105
   */
106
  listen(port: number, callback?: (error?: Error) => void) {
107 16
    return this.app.listen(port, (error) => {
108 16
      if (error) {
109 4
        callback?.(error);
110 4
        this.emit('error', error);
111 1
        return;
112
      }
113 12
      logger.info(`Server listening on port ${port}`);
114 12
      this.emit('initialized');
115
    });
116
  }
117
118
  /**
119
   * Registers all routers
120
   * @param routers {Array<ExpressRouterBean>}
121
   * @private
122
   */
123
  private async registerRouters(routers: Array<ExpressRouterBean>) {
124 18
    return new Promise((resolve, reject) => {
125 18
      Array.from(routers)
126 16
        .map((bean) => bean._instance)
127
        .forEach((instance) => {
128 16
          try {
129
            const {
130
              path,
131
              router,
132 16
            } = instance._routerConfig;
133 16
            logger.debug(`Registering router ${instance._className}`);
134 16
            this.router.use(path, router);
135
          } catch (e) {
136 1
            logger.error(e);
137 1
            reject(new Error(`Router ${instance._className} not initialized correctly`, { cause: e }));
138
          }
139
        });
140 18
      resolve(true);
141
    });
142
  }
143
144
  /**
145
   * Checks if all beans are valid
146
   * @param routerBeans {Array<ExpressRouterBean>}
147
   * @returns {Array<ExpressRouterBean>}
148
   * @throws {Error}
149
   * @private
150
   */
151
  private async checkRouterBeans(routerBeans: Array<ExpressRouterBean>):
152
  Promise<ExpressRouterBean[]> {
153 19
    const invalidBeans = routerBeans
154 19
      .filter(((bean) => !bean._beanUUID))
155 1
      .map((object: any) => object.prototype.constructor.name);
156 19
    if (invalidBeans.length > 0) {
157 1
      return Promise.reject(new Error(`Trying to use something that is not an ExpressBean: ${invalidBeans.join(', ')}`));
158
    }
159 18
    return routerBeans;
160
  }
161
162
  /**
163
   * Gets Express application
164
   * @returns {Express}
165
   */
166
  getApp() {
167 2
    return this.app;
168
  }
169
170
  /**
171
   * Exposes use function of Express application
172
   * @param handlers
173
   */
174
  use(...handlers: any) {
175 1
    this.app.use(...handlers);
176
  }
177
}
178